import java.rmi.RemoteException;
import java.rmi.server.RemoteServer;
import java.rmi.server.ServerNotActiveException;
import java.text.DateFormat;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.Random;
import java.util.Timer;
import java.util.TimerTask;
import java.util.concurrent.Callable;
import java.util.concurrent.ExecutionException;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.TimeoutException;

public class GameBoard implements GameboardInterface {

	static final int WAITING_TIME = 20;
	static final int HEART_BEAT_INTERVAL = 5000;
	static final int HEART_BEAT_DELAY = 0;
	static final int WAIT_TIME_FOR_BACKUPSERVER_ASSIGN = 2000;

	static int serverPort = 30416;

	static final int INI_PRIMARY_SERVER_ID = 1;
	static final int INI_BACKUP_SERVER_ID = 2;

	static final int CONNECT_PRIMARY = 1;
	static final int CONNECT_SECONDARY_FIRST_CLIENT = 2; // when primary is
															// down, the first
															// client to detect
															// it
	static final int CONNECT_PRIMARY_AS_PLAYER_1 = 3; // player id 1 is assigned
														// to server player
														// static final int
														// CONNECT_SECONDARY_OTHER_CLIENT
														// = 4; // after
														// detection of
	// primary down,
	// other clients
	// connect

	static final int BACKUP_SERVER_PORT_INC = 5;

	// support for RMI timeout
	private ExecutorService executor = null;
	// TODO DEBUG_ONLY remove in final version
	int tries = 0;

	// error codes
	static String currentPositionNotFound = " Current position of the player cannot be found. ";
	static String positionOutOfBounds = " Move invalid, current move will take you out of map boundary. ";
	static String playerExistInLocation = " Move invalid, a player alredy exists in the requested location. ";
	static String playerNotRegistered = " You are not a registered player for this game. ";
	static String playerDroppedWithoutNotifying = " Player Droppped from game without notifying. ";
	static String gameNotStartedMoveDenied = " Game has not yet been started. Request to move denied. ";
	static String gameNotStartedDisconnectDenied = " Game has not yet been started. Request to disconnect player denied. ";
	static String backupServerDown = "Backup Game server down";
	static String backupServerPlayerInfoNull = "Player Movement Information received at the backup server is null";
	static String noMoreBackUpServerNotFound = "No more backup servers to support";
	static String duplicateSeqNoRcvd = "Sequence no received is less than or equal to the last received seq no. This might be a duplicate request";

	public HashMap<Integer, Integer> playerTreasureCount; // treasure acquired
															// by each player
	public HashMap<Integer, Coordinates> playerMapCoordinates; // location of
																// each player
	public HashMap<Integer, HashMap<Integer, Integer>> remainingTreasureCoordinates; // remaining
																						// treasure
																						// locations
	public HashMap<Integer, GamePlayerInterface> players; // RMI client objects
	public HashMap<Integer, String> playerIPs; // Client IP address
	public HashMap<Integer, Integer> playerSequenceNo; // Move request seq no of
														// each player

	public int mapSize = 0;
	public int treasure = 0;

	public int gameStartTime;
	public int gameStartRemTime;
	public int lastPlayerId;
	public GameStatusInfo gameStatusInfo;
	private TimerTask timertask;
	private Timer timer;
	private TimerTask heartbeatTimerTask;
	private int remainingTreasure;
	private int updateTime;
	private int backupServerPlayerId;
	private Maze maze;

	/*
	 * Constructor called when new Game starts
	 */
	public GameBoard(int mSize, int treasureCount, int myPort, Maze maze) {
		mapSize = mSize;
		treasure = treasureCount;
		gameStatusInfo = new GameStatusInfo();
		gameStatusInfo.primaryServerPort = myPort;
		gameStatusInfo.backUpServerPort = myPort + BACKUP_SERVER_PORT_INC;
		gameStatusInfo.SetGameStatus(GameStatusInfo.GameStatus.GAME_IDLE);
		updateTime = 1000;
		this.maze = maze;
		printStart();
	}

	/*
	 * Constructor for Backup server
	 */
	public GameBoard(int mSize, int treasureCount, GameStatusInfo gStatusInfo,
			Maze maze) {
		mapSize = mSize;
		treasure = treasureCount;
		gameStatusInfo = gStatusInfo;
		playerIPs = gameStatusInfo.playerIPs;
		players = new HashMap<Integer, GamePlayerInterface>();
		lastPlayerId = gStatusInfo.GetLastPlayerID();
		this.maze = maze;
	}

	/*
	 * Called by Maze for backup servers, to sync player movements after each
	 * player moves. Make sure this is called only for backup servers
	 */
	public synchronized void SyncPlayerMovementInfo(
			PlayerMovementInfo playerMovementInfo) {
		if (playerMovementInfo != null) {
			playerTreasureCount = playerMovementInfo.playerTreasureCount;
			playerMapCoordinates = playerMovementInfo.playerMapCoordinates;
			remainingTreasureCoordinates = playerMovementInfo.remainingTreasureCoordinates;
			remainingTreasure = playerMovementInfo.remainingTreasure;
			playerSequenceNo = playerMovementInfo.playerSequenceNo;
		}

		else {
			System.out.println("Gameboard exception: "
					+ backupServerPlayerInfoNull);
		}
	}

	/*
	 * maze calls this method for backup server to sync game status
	 */
	public void SetGameStatus(GameStatusInfo gameStat) {
		gameStatusInfo = gameStat;
	}

	private void initGameBoard() {
		timertask = new GameTimeScheduler(this);
		timer = new Timer();
		timer.schedule(timertask, 1000, updateTime);
		remainingTreasure = treasure;
		gameStartTime = WAITING_TIME;
		gameStartRemTime = gameStartTime;
		lastPlayerId = 1; // playerID 1 is assigned to self client

		playerTreasureCount = new HashMap<Integer, Integer>();
		playerMapCoordinates = new HashMap<Integer, Coordinates>();
		remainingTreasureCoordinates = new HashMap<Integer, HashMap<Integer, Integer>>();
		players = new HashMap<Integer, GamePlayerInterface>();
		playerIPs = new HashMap<Integer, String>();
		playerSequenceNo = new HashMap<Integer, Integer>();
	}

	private boolean existCoordinate(HashMap<Integer, Coordinates> map,
			Coordinates coord) {
		boolean exist = false;
		for (Map.Entry<Integer, Coordinates> entry : map.entrySet()) {
			if (entry.getValue().equals(coord)) {
				return true;
			}
		}
		return exist;
	}

	/*
	 * add new player to map, and assign playerID to treasure count map
	 */
	private void AddPlayer(GamePlayerInterface gamePlayerInteface) {

		Coordinates coordinates;
		HashMap<Integer, Integer> yCoordinateHashMap;
		int numTreasure;

		// add player ID to playerTreasureCount HashMap
		playerTreasureCount.put(lastPlayerId, 0);

		// add player to the map
		// assign players to random locations in the map
		Random random = new Random();
		while (true) {
			coordinates = new Coordinates(random.nextInt(mapSize),
					random.nextInt(mapSize));
			if (!existCoordinate(playerMapCoordinates, coordinates)) {
				// TODO: init player asset

				// check if initial player location has any treasures
				if (remainingTreasureCoordinates.containsKey(coordinates.x)) {
					yCoordinateHashMap = remainingTreasureCoordinates
							.get(coordinates.x);

					// treasure found
					if (yCoordinateHashMap.containsKey(coordinates.y)) {
						numTreasure = yCoordinateHashMap.get(coordinates.y);

						if (numTreasure > 0) {
							remainingTreasure = remainingTreasure - numTreasure;
							playerTreasureCount.put(lastPlayerId,
									playerTreasureCount.get(lastPlayerId)
											+ numTreasure);
							System.out
									.println("Gameboard message: Acquired treasure, player id: "
											+ lastPlayerId
											+ "No of acquired treasures: "
											+ numTreasure);

							yCoordinateHashMap.put(coordinates.y, 0);
							remainingTreasureCoordinates.put(coordinates.x,
									yCoordinateHashMap);
						}
					}
				}

				playerMapCoordinates.put(lastPlayerId, coordinates);
				break;
			}
		}

		// add players to playerInfoList
		players.put(lastPlayerId, gamePlayerInteface);

		// save IP address of client
		try {
			playerIPs.put(lastPlayerId, RemoteServer.getClientHost());
		} catch (ServerNotActiveException e) {
			e.printStackTrace();
		}

		System.out.println("GameBoard message: New Player Joined ~ Player ID: "
				+ lastPlayerId + " Total Players: " + players.size());
		System.out.println("GameBoard message: Player placed ~ Player id: "
				+ lastPlayerId + " Coordinates: "
				+ coordinates.displayCoordinates());
	}

	@Override
	public synchronized GameStatusInfo ConnectPlayer(
			GamePlayerInterface gamePlayerInteface, int playerType, int playerId)
			throws RemoteException {

		// continue to initialize game only if this is the primary server
		if (((playerType == CONNECT_PRIMARY) || (playerType == CONNECT_PRIMARY_AS_PLAYER_1))
				&& maze.role == Maze.MY_ROLE.PRIMARY_SERVER) {
			int tmpLastPlayerID;
			Random random = new Random();
			int xCoordinate, yCoordinate;
			HashMap<Integer, Integer> yCoordinateHashMap;

			// create a new game if GAME_IDLE
			if (gameStatusInfo.GetGameStatus() == GameStatusInfo.GameStatus.GAME_IDLE) {
				Coordinates coordinates;
				initGameBoard();

				// Assign treasures in the map at random locations
				for (int x = 0; x < treasure; x++) {

					xCoordinate = random.nextInt(mapSize);
					yCoordinate = random.nextInt(mapSize);
					coordinates = new Coordinates(xCoordinate, yCoordinate);

					System.out
							.println("GameBoard message: New treasure ~ Coordinates: "
									+ coordinates.displayCoordinates());

					// if coordinates exist increment the treasure
					if (remainingTreasureCoordinates.containsKey(xCoordinate)) {
						yCoordinateHashMap = remainingTreasureCoordinates
								.get(xCoordinate);
						if (yCoordinateHashMap.containsKey(yCoordinate)) {
							yCoordinateHashMap.put(yCoordinate,
									yCoordinateHashMap.get(yCoordinate) + 1);
							remainingTreasureCoordinates.put(xCoordinate,
									yCoordinateHashMap);
						}
						// else add treasure to new location
						else {
							yCoordinateHashMap.put(yCoordinate, 1);
							remainingTreasureCoordinates.put(xCoordinate,
									yCoordinateHashMap);
						}
					}

					// else add treasure to new location
					else {
						yCoordinateHashMap = new HashMap<Integer, Integer>();
						yCoordinateHashMap.put(yCoordinate, 1);
						remainingTreasureCoordinates.put(xCoordinate,
								yCoordinateHashMap);
					}

				}
				gameStatusInfo
						.SetGameStatus(GameStatusInfo.GameStatus.GAME_STARTING);
				System.out.println("GameBoard message: New Game ~ Created");
			}

			// player cannot join game if the game has already started
			if (gameStatusInfo.GetGameStatus() == GameStatusInfo.GameStatus.GAME_STARTED) {

				gameStatusInfo.SetLastPlayerID(-1);
				gameStatusInfo.SetStartRemTime(-1);
				return gameStatusInfo;

			}

			// if GameStatus is either GAME_STARTING or GAME_IDLE, proceed

			// if server player assign player id 1. Player id 1 is always
			// reserved for server player
			if (playerType == CONNECT_PRIMARY_AS_PLAYER_1) {
				gameStatusInfo.SetLastPlayerID(INI_PRIMARY_SERVER_ID);
				gameStatusInfo.primaryServerPlayerID = INI_PRIMARY_SERVER_ID;
			} else
				gameStatusInfo.SetLastPlayerID(++lastPlayerId);

			// 2nd player to join will be the backup server
			if (lastPlayerId == INI_BACKUP_SERVER_ID) {
				gameStatusInfo.backUpServerPlayerID = lastPlayerId;
				backupServerPlayerId = lastPlayerId;
			}

			// notify the player no map size and treasure count
			gameStatusInfo.SetMapSize(mapSize);
			gameStatusInfo.SetTreasureCount(treasure);

			// gameStart remaing time updated to GameStatusInfo by
			// UpdateGameStartTime()

			// call private method to insert player to map, and init treasure
			// count
			AddPlayer(gamePlayerInteface);

			gameStatusInfo.playerIPs = playerIPs;

			// get LastPlayer's coordinates
			tmpLastPlayerID = lastPlayerId;
			while (tmpLastPlayerID > 0) {
				if (players.containsKey(tmpLastPlayerID)) {
					gameStatusInfo.SetLastPlayerCoordinate(playerMapCoordinates
							.get(tmpLastPlayerID));
					break;
				}
				tmpLastPlayerID--;
			}
		}

		// primary is down, so clients connecting to secondary servers
		if (playerType == CONNECT_SECONDARY_FIRST_CLIENT) {
			if (maze.role == Maze.MY_ROLE.PRIMARY_SERVER) {
				return null;
			} else if (maze.role == Maze.MY_ROLE.BACKUP_SERVER) {
				maze.role = Maze.MY_ROLE.PRIMARY_SERVER;
				System.out
						.println("Gameboard message: I'm ASSIGNED AS PRIMARY");
				System.out
						.println("Gameboard message: First client connected to primary server. Player Id: "
								+ playerId);
				players.put(playerId, gamePlayerInteface);
				// role == PRIMARY

				// remove old primary server
				if (playerIPs.containsKey(gameStatusInfo.primaryServerPlayerID))
					playerIPs.remove(gameStatusInfo.primaryServerPlayerID);

				// this is not necessary, as old primary would have not been
				// added
				// to the new players Hashmap of secondary
				if (players.containsKey(gameStatusInfo.primaryServerPlayerID))
					players.remove(gameStatusInfo.primaryServerPlayerID);

				// set new primary server player ID
				gameStatusInfo.primaryServerPlayerID = gameStatusInfo.backUpServerPlayerID;
				gameStatusInfo.primaryServerPort = gameStatusInfo.backUpServerPort;
				gameStatusInfo.backUpServerPort = gameStatusInfo.backUpServerPort
						+ BACKUP_SERVER_PORT_INC;

				// Init backup server ID
				gameStatusInfo.backUpServerPlayerID = -1;
				backupServerPlayerId = -1;
				// if final outcome is (backUpServerPlayerID = -1), then there
				// is no
				// other clien to assign as backup

				// assign the next backup server
				for (int playerNo = gameStatusInfo.primaryServerPlayerID + 1; playerNo <= gameStatusInfo
						.GetLastPlayerID(); playerNo++) {					
					if (testClientAlive(playerNo)) {
						gameStatusInfo.backUpServerPlayerID = playerNo;
						backupServerPlayerId = playerNo;
						break;
					} else {
						//remove player from list
						players.remove(playerNo);
						playerIPs.remove(playerNo);
					}
				}
				players.get(gameStatusInfo.primaryServerPlayerID)
						.cancelHeartBeat();
				System.out.println("Next backup server "
						+ gameStatusInfo.backUpServerPlayerID);
				// inform all player about the change
				SendMessageToAllClients("Primary crash - New primary and backup server");
				// gamePlayerInteface.ServerStatusUpdate(gameStatusInfo);
				// update latest playermoventinfo to backup
				PlayerMovementInfo packInfo = new PlayerMovementInfo();
				packInfo.playerMapCoordinates = playerMapCoordinates;
				packInfo.playerTreasureCount = playerTreasureCount;
				packInfo.remainingTreasureCoordinates = remainingTreasureCoordinates;
				packInfo.moveSuccess = true;
				packInfo.remainingTreasure = remainingTreasure;
				players.get(gameStatusInfo.backUpServerPlayerID)
						.SecondaryServerReceiveUPD(packInfo);
				initHeartBeat();
			}
		}

		return gameStatusInfo;
	}

	/*
	 * private method to show the start of game
	 */
	private void printStart() {
		Date dt = new Date();
		DateFormat dtFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
		System.out
				.println("***************************** Start of Game ************************************");
		System.out.println("****************** Creation Time:  : "
				+ dtFormat.format(dt) + " ***********************");
	}

	/*
	 * private method to show the end of game
	 */
	private void printEnd() {
		Date dt = new Date();
		DateFormat dtFormat = new SimpleDateFormat("yyyy/MM/dd HH:mm:ss");
		System.out
				.println("\n***************************** End of Game ************************************");
		System.out.println("*********************** End Time:  : "
				+ dtFormat.format(dt) + " *********************\n");
	}

	private void printResults() {
		System.out
				.println("\n******************************* Results **************************************");
		System.out.println("Player\t\tNo of Treasures");

		int tmpPlayerID = lastPlayerId;

		while (tmpPlayerID > 0) {
			if (playerTreasureCount.containsKey(tmpPlayerID)) {
				System.out.println(tmpPlayerID + "\t\t"
						+ playerTreasureCount.get(tmpPlayerID));
			}
			tmpPlayerID--;

		}
	}

	// Thread to inform backup
	public class InformBackup implements Callable<Object> {
		private PlayerMovementInfo playerInfo;
		private GamePlayerInterface player;

		public InformBackup(PlayerMovementInfo infos, GamePlayerInterface dest) {
			playerInfo = infos;
			player = dest;
		}

		@Override
		public Object call() throws Exception {
			player.SecondaryServerReceiveUPD(playerInfo);
			return null;
		}

	}

	@Override
	public synchronized PlayerMovementInfo RequestMove(
			PlayerMovementInfo packInfo, int PlayerID) throws Exception {

		Coordinates coordinates = null;
		Coordinates originalCoordinates = null;
		int remTreasureInCoordinate;
		int tmpPlayerID;
		HashMap<Integer, Integer> yCoordinateHashMap;

		System.out
				.println("-------------------------------------------------------------------------------");

		System.out
				.println("GameBoard message: Move request received, Player id: "
						+ PlayerID + " Sequence no: " + packInfo.mySequenceNo);
		// check for player movement sequence No
		// if received sequence no is not greater than last received seq no
		if (playerSequenceNo.containsKey(PlayerID)) {
			if (!(playerSequenceNo.get(PlayerID) < packInfo.mySequenceNo)) {
				System.out
						.println("GameBoard exception: " + duplicateSeqNoRcvd);
				System.out.println("Player id: " + PlayerID
						+ " Received seq No: " + packInfo.mySequenceNo
						+ " Last received seq: "
						+ playerSequenceNo.get(PlayerID));
				packInfo.ServerMsg = ("GameBoard exception: " + duplicateSeqNoRcvd);
				packInfo.moveSuccess = false;
				packInfo.playerSequenceNo = playerSequenceNo;
				return packInfo;
			} else {
				playerSequenceNo.put(PlayerID, packInfo.mySequenceNo);
			}

		}

		// check if game is started. if not reject move
		if (gameStatusInfo.GetGameStatus() != GameStatusInfo.GameStatus.GAME_STARTED) {
			packInfo.ServerMsg = ("GameBoard exception: " + "player id: "
					+ PlayerID + gameNotStartedMoveDenied);
			packInfo.moveSuccess = false;
			return packInfo;
		}

		coordinates = playerMapCoordinates.get(PlayerID);
		if (coordinates == null) {
			System.err.println("GameBoard exception: " + "Player ID: "
					+ PlayerID + "\n" + playerNotRegistered);
			packInfo.ServerMsg = ("GameBoard exception: " + playerNotRegistered);
			packInfo.moveSuccess = false;
			return packInfo;
		}

		// check if the move is valid. If invalid dont accept move, return
		System.out
				.println("GameBoard message:  player " + PlayerID
						+ " current coordinates " + coordinates.x + " "
						+ coordinates.y);

		// store current location before changing it
		originalCoordinates = new Coordinates(coordinates.x, coordinates.y);

		if (packInfo.direction == PlayerMovementInfo.Direction.NO_MOVE) {

		} else if ((packInfo.direction == PlayerMovementInfo.Direction.DIR_UP && (coordinates.y - 1) < 0)
				|| (packInfo.direction == PlayerMovementInfo.Direction.DIR_DOWN && (coordinates.y + 1) >= mapSize)
				|| (packInfo.direction == PlayerMovementInfo.Direction.DIR_LEFT && (coordinates.x - 1) < 0)
				|| (packInfo.direction == PlayerMovementInfo.Direction.DIR_RIGHT && (coordinates.x + 1) >= mapSize)) {
			packInfo.ServerMsg = ("GameBoard exception: " + positionOutOfBounds);
			System.out
					.println("GameBoard message: Move request denied ~ Player id  "
							+ PlayerID + " Reason: " + positionOutOfBounds);
		} else {

			if (packInfo.direction == PlayerMovementInfo.Direction.DIR_UP)
				--(coordinates.y);
			if (packInfo.direction == PlayerMovementInfo.Direction.DIR_DOWN)
				++(coordinates.y);
			if (packInfo.direction == PlayerMovementInfo.Direction.DIR_LEFT)
				--(coordinates.x);
			if (packInfo.direction == PlayerMovementInfo.Direction.DIR_RIGHT)
				++(coordinates.x);

			// check if another player resides in new location, if exist invalid
			// move return

			tmpPlayerID = lastPlayerId;
			while (tmpPlayerID > 0) {

				// same player. So will have to cater for no move, exit while
				// and continue
				if (PlayerID == tmpPlayerID) {
					tmpPlayerID--;
					continue;
				}

				if (playerMapCoordinates.containsKey(tmpPlayerID)) {
					if (playerMapCoordinates.get(tmpPlayerID).equals(
							coordinates)) {
						System.out
								.println("GameBoard message: Move request deined ~ Player id:  "
										+ PlayerID
										+ " Reason: "
										+ playerExistInLocation
										+ " player ID: " + tmpPlayerID);

						// restore the original coordinates
						coordinates.CopyCoordinates(originalCoordinates);

						// populate PlayerMovementInfo details
						packInfo.ServerMsg = ("GameBoard exception: " + playerExistInLocation);
						packInfo.moveSuccess = false;
						packInfo.playerMapCoordinates = playerMapCoordinates;
						packInfo.playerTreasureCount = playerTreasureCount;
						packInfo.remainingTreasureCoordinates = remainingTreasureCoordinates;
						packInfo.playerSequenceNo = playerSequenceNo;

						return packInfo;
					}
				}
				tmpPlayerID--;
			}

			// move valid
			// update new coordinates of player
			playerMapCoordinates.remove(PlayerID);
			playerMapCoordinates.put(PlayerID, coordinates);
			System.out.println("GameBoard message: New location ~ Player id:  "
					+ PlayerID + " Location: "
					+ coordinates.displayCoordinates());

			packInfo.ServerMsg = "Move Success. No treasure acquired.";
			// check for treasure in new coordinates

			if (remainingTreasureCoordinates.containsKey(coordinates.x)) {
				yCoordinateHashMap = remainingTreasureCoordinates
						.get(coordinates.x);

				// tresure found
				if (yCoordinateHashMap.containsKey(coordinates.y)) {

					remTreasureInCoordinate = yCoordinateHashMap
							.get(coordinates.y);

					if (remTreasureInCoordinate > 0) {
						yCoordinateHashMap.put(coordinates.y, 0);
						remainingTreasureCoordinates.put(coordinates.x,
								yCoordinateHashMap);
						remainingTreasure = remainingTreasure
								- remTreasureInCoordinate;

						// add treasure count to player
						playerTreasureCount.put(PlayerID,
								playerTreasureCount.get(PlayerID)
										+ remTreasureInCoordinate);
						System.out
								.println("GameBoard message: Treasure Acquired ~ Player id:  "
										+ PlayerID
										+ " No of Treasure: "
										+ playerTreasureCount.get(PlayerID));
						packInfo.ServerMsg = "Move Sucess. "
								+ remTreasureInCoordinate
								+ " treasures acquired.";
					}
				}
			}

		}

		// check if all treasures collected, if so end game
		if (remainingTreasure <= 0) {
			EndGame();
		}

		// update values and return PlayerMovementInfo
		packInfo.playerMapCoordinates = playerMapCoordinates;
		packInfo.playerTreasureCount = playerTreasureCount;
		packInfo.remainingTreasureCoordinates = remainingTreasureCoordinates;
		packInfo.moveSuccess = true;
		packInfo.remainingTreasure = remainingTreasure;
		packInfo.playerSequenceNo = playerSequenceNo;

		// check there exists an backup server
		if (backupServerPlayerId != -1) {
			// inform backup server regarding the move
			int no_of_retries = 0;
			// will try sending backup server, if failed, try sending to new
			// backup server
			while (no_of_retries < 2) {
				try {

					if (!players.containsKey(backupServerPlayerId)) {
						System.out.println("GameBoard exception: "
								+ noMoreBackUpServerNotFound);
						break;
					}

					// @RMI-timeout
					if (executor == null) {
						executor = Executors.newSingleThreadExecutor();
					}
					Future<Object> future = executor.submit(new InformBackup(
							packInfo, players.get(backupServerPlayerId)));
					future.get(Maze.RMI_WAITING_TIME, TimeUnit.SECONDS);

					// send successfull, exit loop
					break;
				}

				// TODO BackUp server down
				catch (InterruptedException | TimeoutException
						| ExecutionException e) {

					// try sending to the new backup server once more
					no_of_retries++;
					System.out.println("GameBoard exception: "
							+ backupServerDown + " Player id: "
							+ backupServerPlayerId);
					// System.out.println("GameBoard exception: " + "message: "
					// + e.getMessage());
					handleBackupCrash();

					// wait, give time for new backup server to create server
					Thread.sleep(WAIT_TIME_FOR_BACKUPSERVER_ASSIGN);

				}
			}
		} else {
			System.out.print("GameBoard message: No backup server assigned");
		}
		return packInfo; // return to client
	}

	private boolean testClientAlive(int id) {
		boolean alive = true;
		try {
			alive = players.get(id).ping();
		} catch (RemoteException e) {
			System.err.println("Player " + id + " has died");
			return false;
		}
		return alive;
	}

	private void handleBackupCrash() {
		players.remove(backupServerPlayerId);
		playerIPs.remove(backupServerPlayerId);
		// assign new backup server
		for (int tmpPlayerId = backupServerPlayerId + 1; tmpPlayerId <= lastPlayerId; tmpPlayerId++) {
			if (players.containsKey(tmpPlayerId)) {
				if (testClientAlive(tmpPlayerId)) {
					backupServerPlayerId = tmpPlayerId;
					// check if tmpPlayerId alive
					gameStatusInfo.backUpServerPlayerID = tmpPlayerId;
					System.out
							.println("GameBoard message: New Backup Server assigned: PlayerID: "
									+ backupServerPlayerId);
					break;
				} else {
					//remove player from list
					players.remove(tmpPlayerId);
					playerIPs.remove(tmpPlayerId);
				}
			}
		}
		// inform all about change in backup server. Backup server will assign
		// itself as backup
		SendMessageToAllClients("Backup Server changed");
	}

	/*
	 * private method to end game
	 */
	private void EndGame() {
		gameStatusInfo.SetGameStatus(GameStatusInfo.GameStatus.GAME_IDLE);
		// return player tresure count map, to display final results
		gameStatusInfo.playerTreasureCount = playerTreasureCount;

		printEnd();

		// notify all player that game has ended
		SendMessageToAllClients("GAME END");

		// publish final results
		printResults();

	}

	public void UpdateGameStartTime() {

		if (gameStatusInfo.GetGameStatus() == GameStatusInfo.GameStatus.GAME_STARTING) {
			System.out
					.println("Game start remaining time: " + gameStartRemTime);
			gameStartRemTime--;

			gameStatusInfo.SetStartRemTime(gameStartRemTime);

			if (gameStartRemTime == 0) {
				gameStatusInfo
						.SetGameStatus(GameStatusInfo.GameStatus.GAME_STARTED);
				gameStatusInfo.players = players;
				System.out.println("GameBoard message: Game Started");

				// Notify all player that game starts
				SendMessageToAllClients("Game Start");

				// send init game state to backup server
				PlayerMovementInfo pmi = new PlayerMovementInfo();
				pmi.playerMapCoordinates = playerMapCoordinates;
				pmi.playerTreasureCount = playerTreasureCount;
				pmi.remainingTreasureCoordinates = remainingTreasureCoordinates;
				pmi.remainingTreasure = remainingTreasure;
				pmi.ServerMsg = "Init game";
				try {
					players.get(backupServerPlayerId)
							.SecondaryServerReceiveUPD(pmi);
				} catch (RemoteException e) {
					System.err.println("Backup server crash during game setup");
				}
				initHeartBeat();
			}
		}
	}

	/*
	 * Call ServerStatusUpdate for all clients
	 */
	private void SendMessageToAllClients(String msg) {
		for (int playerID = 1; playerID <= lastPlayerId; playerID++) {
			try {
				// the player might have already been removed
				if (players.containsKey(playerID)) {
					System.out.println("GameBoard message: Send player Id "
							+ playerID + ": " + msg);
					players.get(playerID).ServerStatusUpdate(gameStatusInfo);
				}
			} catch (RemoteException e) {
				System.err.println("GameBoard exception: " + e.toString());
				System.err.println("GameBoard exception: " + "Player ID: "
						+ playerID + "\n" + playerDroppedWithoutNotifying);
				players.remove(playerID);
				playerIPs.remove(playerID);
			}
		}
	}

	@Override
	public int signalHeartBeat() throws Exception {
		// System.out.println("Send heartbeat to backup");
		return 0;
	}

	public GamePlayerInterface getBackupServerInterface() {
		return players.get(backupServerPlayerId);
	}

	private void initHeartBeat() {
		if (timer == null)
			timer = new Timer();
		timer.purge();
		heartbeatTimerTask = new HeartBeat(this);
		timer.schedule(heartbeatTimerTask, HEART_BEAT_DELAY,
				HEART_BEAT_INTERVAL);
	}

	public class HeartBeat extends TimerTask {
		private GameBoard gb;

		public HeartBeat(GameBoard gboard) {
			gb = gboard;
		}

		@Override
		public void run() {
			try {
				gb.getBackupServerInterface().signalHeartBeat();
			} catch (RemoteException e) {
				// TODO backup crashes by heartbeat
				System.err
						.println("Backup server crash - detected by heartbeat");
				handleBackupCrash();
				int no_of_retries = 0;
				// will try sending backup server, if failed, try sending to new
				// backup server
				while (no_of_retries < 2) {
					try {

						if (!players.containsKey(backupServerPlayerId)) {
							System.out.println("GameBoard exception: "
									+ noMoreBackUpServerNotFound);
							break;
						}

						// @RMI-timeout
						if (executor == null) {
							executor = Executors.newSingleThreadExecutor();
						}
						PlayerMovementInfo packInfo = new PlayerMovementInfo();
						packInfo.playerMapCoordinates = playerMapCoordinates;
						packInfo.playerTreasureCount = playerTreasureCount;
						packInfo.remainingTreasureCoordinates = remainingTreasureCoordinates;
						packInfo.moveSuccess = true;
						packInfo.remainingTreasure = remainingTreasure;
						Future<Object> future = executor
								.submit(new InformBackup(packInfo, players
										.get(backupServerPlayerId)));
						future.get(Maze.RMI_WAITING_TIME, TimeUnit.SECONDS);

						// send successfull, exit loop
						break;
					}

					// TODO BackUp server down
					catch (InterruptedException | TimeoutException
							| ExecutionException ex) {

						// try sending to the new backup server once more
						no_of_retries++;
						System.out.println("GameBoard timeout exception: "
								+ backupServerDown + " Player id: "
								+ backupServerPlayerId);
						// System.out.println("GameBoard exception: " +
						// "message: " + e.getMessage());
						handleBackupCrash();

						// wait, give time for new backup server to create
						// server
						try {
							Thread.sleep(WAIT_TIME_FOR_BACKUPSERVER_ASSIGN);
						} catch (InterruptedException e1) {
							// TODO Auto-generated catch block
							e1.printStackTrace();
						}

					}
				}
				// e.printStackTrace();
			}
		}
	}

	public void setPlayersRemoteObject(HashMap<Integer, GamePlayerInterface> map) {
		players = map;
	}

}
